Custom Visual Example - Aster Plot

The following examples shows several of the key aspects of using the CV API (2.0) through the Aster Plot example found in the Marketplace inside the application.

Code Break Down

Main Function

The main function (row 1) is the key entry point.

  • It registers a listener for the render event, calling the render function on row 6
  • It sets a series of custom CSS styles - which calls another function on row 143

Render Function

Key aspects of the graphic wire up include:

  • The render function (row 6), which is called as each chart is rendered in the client, starts by getting access to the current data set (on row 8) from the trellis function in the data object.
    • within the data set, the list of data points in the query result are accessed on row 9
  • The parent object that will house the drawing is found on row 10 from the canvas object.
  • the data points from row 9 are processed in rows 27-39, where the numeric values behind each data point are extracted and injected into an array "visualModel", which was substantiated on row 22.
  • sizing for the visual is calculated based on the canvas size in rows 41-44
  • a classic D3 pie visualization is setup and drawn in rows 47 - 73.
  • On rows 75-78, the visualModel items are pushed into the graphic objects
  • The merging of the data points with the D3 graphic logic is resolved in 82-101. Here, the data points are used to drive colors, tooltips (for mouse over events); triggering of the Pyramid context menus; and the selection state.

function main() {    
	cvApi2.canvas.setCustomCssStyle(defineStyles());
	cvApi2.canvas.addEventListener(cvApi2.enums.events.Render, render);
}

function render() {
	//get the trellised data set for rendering 
	var currentTrellisedData = cvApi2.resultSet.data.getCurrentTrellisData();
	var dataPoints = currentTrellisedData.datapoints
	var element = cvApi2.canvas.getHTMLElement();
	var d3 = cvApi2.externalLibraries.d3;

 
	//get value range (min and max value from the given array)
	var valueRange = d3.array.extent(
	dataPoints.map(function (dp) { return dp.numerics.value.rawValue }));
     
	//define the scale
	var scoreScale = d3.scale.scaleLinear()
		.domain(valueRange).range([20, 100]);
 
	var visualModel = {
	items: [],
	};

	//get data points as object for d3
	for (var i = 0; i < dataPoints.length; i++) {
		var dp = dataPoints[i];
		var dpValue = dp.numerics.value.rawValue;
		visualModel.items.push({
		datapoint: dp,
		value: dpValue,
		color: dp.numerics.color?.[0]?.formattedValue,
		weight: dp.numerics.size?.[0]?.rawValue,
		score: scoreScale(dpValue),
		width: dp.numerics.size?.[0]?.rawValue,
		label: dp.numerics.value.formattedValue,
		});
	}

	var width = cvApi2.canvas.width;
	var height = cvApi2.canvas.height;
	var radius = Math.min(width, height) / 2;
	var innerRadius = 0.40 * radius;

	//pie based visual
	var pie = d3.shape.pie()
		.sort(null)
		.value(function (d) { return d.width; });

	//calculate arcs
	var arc = d3.shape.arc()
		.innerRadius(innerRadius)
		.outerRadius(function (d) {
			return (radius - innerRadius) * (d.data.score / 100.0) + innerRadius;
		});

	var outlineArc = d3.shape.arc()
		.innerRadius(innerRadius)
		.outerRadius(radius);

	//select the SVG element
	var root = d3.selection
		.select(element)
		.selectAll('g.root')
		.data(function (d) { return [d]; });

	root = root
		.enter()
		.append("g")
		.attr('class', 'root')
		.merge(root)
		.attr('transform', 'translate(' + (width / 2) + ',' + (height / 2) + ')');

	//solid arc appearance
		var path = root
		.selectAll(".solid-arc")
		.data(pie(visualModel.items));

	path.exit().remove();

	path = path
		.enter()
		.append("path")
		.attr("stroke", "#d3d3d3")
		.merge(path)
		.attr("fill", function (d) { return d.data.color; })
		.attr("d", arc)
		.attr("class", function (d) {
			var className = 'solid-arc';
				if (cvApi2.canvas.isSelectionEnabled()) {
					if (d.data.datapoint.isSelected)
					className += ' selected'
					else className += ' non-selected'
				}
			return className;
			})
		.on("mouseover", function (d) { d.data.datapoint.showTooltip(d3.selection.event); })
		.on("mouseout", function (d) { d.data.datapoint.hideTooltip(d3.selection.event); })
		.on("contextmenu", function (d) { d.data.datapoint.showContextMenu(d3.selection.event); })
		.on("click", function (d) { d.data.datapoint.select(); })

	//outer line appearance
	var outerPath = root
		.selectAll(".outline-arc")
		.data(pie(visualModel.items));
		outerPath.exit().remove();

	outerPath = outerPath
		.enter()
		.append("path")
		.attr("fill", "none")
		.attr("stroke", "#d3d3d3")
		.attr("class", "outline-arc")
		.merge(outerPath)
		.attr("d", outlineArc);

	// calculate the weighted mean score
	var score =
		visualModel.items.reduce(function (a, b) {
			return a + (b.score * b.weight);
		}, 0) /
		visualModel.items.reduce(function (a, b) {
			return a + b.weight;
		}, 0);

	//text appearance
	var text = root
		.selectAll('text')
		.data(function (d) { return [d]; });

	text = text
		.enter()
		.append("text")
		.attr("class", "aster-score")
		.attr("dy", ".35em")
		.attr("text-anchor", "middle")
		.merge(text)
		.text(function () { return isNaN(score) ? '-' : Math.round(score) });
}

// this function should return css string (if any) for custom style
function defineStyles(){ 
	return '.axis path,'+
		'.axis line {'+
		'fill: none;'+
		'stroke: #000;'+
		'shape-rendering: crispEdges;}'+
		'.bar {'+
		'fill: orange;}'+
		'.solid-arc:hover {'+
		'stroke: black ;'+
		'stroke-width:1.5px;}'+
		'.solid-arc {'+
		'-moz-transition: all 0.3s;'+
		'-o-transition: all 0.3s;'+
		'-webkit-transition: all 0.3s;'+
		'transition: all 0.3s;}'+
		'.solid-arc.selected {'+
		'opacity: 1;}'+
		'.solid-arc.non-selected {'+
		'opacity: 0.5;}'+
		'.x.axis path {'+
		'display: none;}'+
		'.aster-score { '+
		'line-height: 1;'+
		'font-weight: bold;'+
		'font-size: 500%;}';
}